/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.pekko.persistence.testkit.query

import scala.collection.immutable.Seq
import scala.concurrent.duration._

import org.apache.pekko
import pekko.Done
import pekko.actor.testkit.typed.scaladsl.LogCapturing
import pekko.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import pekko.actor.typed.ActorRef
import pekko.persistence.Persistence
import pekko.persistence.query.NoOffset
import pekko.persistence.query.PersistenceQuery
import pekko.persistence.query.typed.EventEnvelope
import pekko.persistence.testkit.PersistenceTestKitPlugin
import pekko.persistence.testkit.internal.InMemStorageExtension
import pekko.persistence.testkit.query.javadsl.{ PersistenceTestKitReadJournal => JavaPersistenceTestKitReadJournal }
import pekko.persistence.testkit.query.scaladsl.PersistenceTestKitReadJournal
import pekko.persistence.typed.PersistenceId
import pekko.persistence.typed.scaladsl.Effect
import pekko.persistence.typed.scaladsl.EventSourcedBehavior
import pekko.stream.testkit.scaladsl.TestSink

import org.scalatest.BeforeAndAfterEach
import org.scalatest.wordspec.AnyWordSpecLike

import com.typesafe.config.ConfigFactory

object EventsBySliceSpec {
  val config = PersistenceTestKitPlugin.config.withFallback(
    ConfigFactory.parseString("""
    pekko.loglevel = DEBUG
    pekko.loggers = ["org.apache.pekko.testkit.SilenceAllTestEventListener"]
    pekko.persistence.testkit.events.serialize = off
      """))

  case class Command(evt: Seq[String], ack: ActorRef[Done])
  object Command {
    def apply(evt: String, ack: ActorRef[Done]): Command = Command(
      Seq(evt), ack
    )
  }
  case class State()

  def testBehaviour(persistenceId: String) = {
    EventSourcedBehavior[Command, String, State](
      PersistenceId.ofUniqueId(makeFullPersistenceId(persistenceId)),
      State(),
      (_, command) =>
        Effect.persist(command.evt).thenRun { _ =>
          command.ack ! Done
        },
      (state, _) => state
    )
  }

  def makeFullPersistenceId(persistenceId: String) = {
    s"Test|$persistenceId"
  }
}

class EventsBySliceSpec
    extends ScalaTestWithActorTestKit(EventsByPersistenceIdSpec.config)
    with LogCapturing
    with AnyWordSpecLike
    with BeforeAndAfterEach {
  import EventsBySliceSpec._

  implicit val classic: pekko.actor.ActorSystem = system.classicSystem

  private val persistenceQuery = PersistenceQuery(system)

  private val queries =
    persistenceQuery.readJournalFor[PersistenceTestKitReadJournal](
      PersistenceTestKitReadJournal.Identifier)

  private val queriesJava =
    persistenceQuery.getReadJournalFor(
      classOf[JavaPersistenceTestKitReadJournal],
      JavaPersistenceTestKitReadJournal.Identifier)

  def setup(persistenceId: String): ActorRef[Command] = {
    val probe = createTestProbe[Done]()
    val ref = setupEmpty(persistenceId)
    ref ! Command(s"$persistenceId-1", probe.ref)
    ref ! Command(s"$persistenceId-2", probe.ref)
    ref ! Command(s"$persistenceId-3", probe.ref)
    probe.expectMessage(Done)
    probe.expectMessage(Done)
    probe.expectMessage(Done)
    ref
  }

  def setupBatched(persistenceId: String): ActorRef[Command] = {
    val probe = createTestProbe[Done]()
    val ref = setupEmpty(persistenceId)
    ref ! Command(Seq(s"$persistenceId-1", s"$persistenceId-2", s"$persistenceId-3"), probe.ref)
    probe.expectMessage(Done)
    ref
  }

  def setupEmpty(persistenceId: String): ActorRef[Command] = {
    spawn(testBehaviour(persistenceId))
  }

  private lazy val persistence = Persistence(system)
  private lazy val numberOfSlices = persistence.numberOfSlices

  private lazy val eventStorage = InMemStorageExtension(system).storageFor(PersistenceTestKitPlugin.PluginId)

  override protected def beforeEach(): Unit = {
    super.beforeEach()
    eventStorage.clearAll()
  }

  "Persistent test kit live query EventsBySlice" must {
    "find new events" in {
      val ackProbe = createTestProbe[Done]()
      val ref = setup("c")
      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe = src.map(_.event).runWith(TestSink[String]()).request(5).expectNext("c-1", "c-2", "c-3")

      ref ! Command("c-4", ackProbe.ref)
      ackProbe.expectMessage(Done)

      probe.expectNext("c-4")
    }

    "find new events (Java DSL)" in {
      val ackProbe = createTestProbe[Done]()
      val ref = setup("c")
      val src = queriesJava.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset).asScala
      val probe = src.map(_.event).runWith(TestSink[String]()).request(5).expectNext("c-1", "c-2", "c-3")

      ref ! Command("c-4", ackProbe.ref)
      ackProbe.expectMessage(Done)

      probe.expectNext("c-4")
    }

    "find new events after batched setup" in {
      val ackProbe = createTestProbe[Done]()
      val ref = setupBatched("d")
      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe = src.map(_.event).runWith(TestSink[String]()).request(5).expectNext("d-1", "d-2", "d-3")

      ref ! Command("d-4", ackProbe.ref)
      ackProbe.expectMessage(Done)

      probe.expectNext("d-4")
    }

    "find new events after demand request" in {
      val ackProbe = createTestProbe[Done]()
      val ref = setup("e")
      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe =
        src.map(_.event).runWith(TestSink[String]()).request(2).expectNext("e-1", "e-2").expectNoMessage(100.millis)

      ref ! Command("e-4", ackProbe.ref)
      ackProbe.expectMessage(Done)

      probe.expectNoMessage(100.millis).request(5).expectNext("e-3").expectNext("e-4")
    }

    "include timestamp in EventEnvelope" in {
      setup("n")

      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe = src.runWith(TestSink[EventEnvelope[String]]())

      probe.request(5)
      probe.expectNext().timestamp should be > 0L
      probe.expectNext().timestamp should be > 0L
      probe.cancel()
    }

    "not complete for empty slice" in {
      val ackProbe = createTestProbe[Done]()
      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe =
        src.map(_.event).runWith(TestSink[String]()).request(2)

      probe.expectNoMessage(200.millis) // must not complete

      val ref = setupEmpty("o")
      ref ! Command("o-1", ackProbe.ref)
      ackProbe.expectMessage(Done)

      probe.cancel()
    }

    "find new events in order that they were persisted when the slice range is used by multiple persistence IDs" in {
      val ackProbe = createTestProbe[Done]()
      val src = queries.eventsBySlices[String]("Test", 0, numberOfSlices - 1, NoOffset)
      val probe = src
        .map { ee =>
          (ee.persistenceId, ee.event)
        }
        .runWith(TestSink[(String, Any)]())

      val ref2 = setupEmpty("f2")
      ref2 ! Command(Seq("f2-1", "f2-2"), ackProbe.ref)
      ackProbe.expectMessage(Done)
      probe.request(2).expectNextN(Seq((makeFullPersistenceId("f2"), "f2-1"), (makeFullPersistenceId("f2"), "f2-2")))

      val ref1 = setupEmpty("f1")
      ref1 ! Command(Seq("f1-1", "f1-2"), ackProbe.ref)
      ackProbe.expectMessage(Done)
      probe.request(2).expectNextN(Seq((makeFullPersistenceId("f1"), "f1-1"), (makeFullPersistenceId("f1"), "f1-2")))
    }

    "find new events only for slices that were requested" in {
      val persistenceId1 = "g"
      val persistenceId2 = "h"

      val slice1 = persistence.sliceForPersistenceId(makeFullPersistenceId(persistenceId1))
      val slice2 = persistence.sliceForPersistenceId(makeFullPersistenceId(persistenceId2))
      slice1 should not be slice2

      setup(persistenceId1)
      setup(persistenceId2)

      def assertSlice(slice: Int, persistenceId: String) = {
        val src = queries.eventsBySlices[String]("Test", slice, slice, NoOffset)
        val fullPersistenceId = makeFullPersistenceId(persistenceId)
        src
          .map { ee =>
            (ee.persistenceId, ee.event)
          }
          .runWith(TestSink[(String, Any)]())
          .request(3)
          .expectNextN(Seq((fullPersistenceId, s"$persistenceId-1"), (fullPersistenceId, s"$persistenceId-2"),
            (fullPersistenceId, s"$persistenceId-3")))
      }

      assertSlice(slice1, persistenceId1)
      assertSlice(slice2, persistenceId2)
    }
  }
}
